// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/service_worker/service_worker_read_from_cache_job.h"

#include <string>
#include <vector>

#include "base/threading/thread_task_runner_handle.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "content/browser/service_worker/service_worker_disk_cache.h"
#include "content/browser/service_worker/service_worker_metrics.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_status.h"

namespace content {

ServiceWorkerReadFromCacheJob::ServiceWorkerReadFromCacheJob(
    net::URLRequest* request,
    net::NetworkDelegate* network_delegate,
    ResourceType resource_type,
    base::WeakPtr<ServiceWorkerContextCore> context,
    const scoped_refptr<ServiceWorkerVersion>& version,
    int64_t resource_id)
    : net::URLRequestJob(request, network_delegate)
    , resource_type_(resource_type)
    , resource_id_(resource_id)
    , context_(context)
    , version_(version)
    , weak_factory_(this)
{
    DCHECK(version_);
    DCHECK(resource_type_ == RESOURCE_TYPE_SCRIPT || (resource_type_ == RESOURCE_TYPE_SERVICE_WORKER && version_->script_url() == request_->url()));
}

ServiceWorkerReadFromCacheJob::~ServiceWorkerReadFromCacheJob()
{
}

void ServiceWorkerReadFromCacheJob::Start()
{
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::Bind(&ServiceWorkerReadFromCacheJob::StartAsync, weak_factory_.GetWeakPtr()));
}

void ServiceWorkerReadFromCacheJob::Kill()
{
    if (has_been_killed_)
        return;
    weak_factory_.InvalidateWeakPtrs();
    has_been_killed_ = true;
    reader_.reset();
    context_.reset();
    http_info_io_buffer_ = nullptr;
    http_info_.reset();
    range_response_info_.reset();
    net::URLRequestJob::Kill();
}

net::LoadState ServiceWorkerReadFromCacheJob::GetLoadState() const
{
    if (reader_.get() && reader_->IsReadPending())
        return net::LOAD_STATE_READING_RESPONSE;
    return net::LOAD_STATE_IDLE;
}

bool ServiceWorkerReadFromCacheJob::GetCharset(std::string* charset)
{
    if (!http_info())
        return false;
    return http_info()->headers->GetCharset(charset);
}

bool ServiceWorkerReadFromCacheJob::GetMimeType(std::string* mime_type) const
{
    if (!http_info())
        return false;
    return http_info()->headers->GetMimeType(mime_type);
}

void ServiceWorkerReadFromCacheJob::GetResponseInfo(
    net::HttpResponseInfo* info)
{
    if (!http_info())
        return;
    *info = *http_info();
}

int ServiceWorkerReadFromCacheJob::GetResponseCode() const
{
    if (!http_info())
        return -1;
    return http_info()->headers->response_code();
}

void ServiceWorkerReadFromCacheJob::SetExtraRequestHeaders(
    const net::HttpRequestHeaders& headers)
{
    std::string value;
    std::vector<net::HttpByteRange> ranges;
    if (!headers.GetHeader(net::HttpRequestHeaders::kRange, &value) || !net::HttpUtil::ParseRangeHeader(value, &ranges)) {
        return;
    }

    // If multiple ranges are requested, we play dumb and
    // return the entire response with 200 OK.
    if (ranges.size() == 1U)
        range_requested_ = ranges[0];
}

int ServiceWorkerReadFromCacheJob::ReadRawData(net::IOBuffer* buf,
    int buf_size)
{
    DCHECK_NE(buf_size, 0);
    DCHECK(!reader_->IsReadPending());
    TRACE_EVENT_ASYNC_BEGIN1("ServiceWorker",
        "ServiceWorkerReadFromCacheJob::ReadRawData",
        this,
        "URL", request_->url().spec());
    reader_->ReadData(buf, buf_size,
        base::Bind(&ServiceWorkerReadFromCacheJob::OnReadComplete,
            weak_factory_.GetWeakPtr()));
    return net::ERR_IO_PENDING;
}

void ServiceWorkerReadFromCacheJob::StartAsync()
{
    TRACE_EVENT_ASYNC_BEGIN1("ServiceWorker",
        "ServiceWorkerReadFromCacheJob::ReadInfo", this,
        "URL", request_->url().spec());
    if (!context_) {
        // NotifyStartError is not safe to call synchronously in Start.
        NotifyStartError(
            net::URLRequestStatus(net::URLRequestStatus::FAILED, net::ERR_FAILED));
        return;
    }

    // Create a response reader and start reading the headers,
    // we'll continue when thats done.
    if (is_main_script())
        version_->embedded_worker()->OnScriptReadStarted();
    reader_ = context_->storage()->CreateResponseReader(resource_id_);
    http_info_io_buffer_ = new HttpResponseInfoIOBuffer;
    reader_->ReadInfo(
        http_info_io_buffer_.get(),
        base::Bind(&ServiceWorkerReadFromCacheJob::OnReadInfoComplete,
            weak_factory_.GetWeakPtr()));
}

const net::HttpResponseInfo* ServiceWorkerReadFromCacheJob::http_info() const
{
    if (!http_info_)
        return nullptr;
    if (range_response_info_)
        return range_response_info_.get();
    return http_info_.get();
}

void ServiceWorkerReadFromCacheJob::OnReadInfoComplete(int result)
{
    if (!http_info_io_buffer_->http_info) {
        DCHECK_LT(result, 0);
        ServiceWorkerMetrics::CountReadResponseResult(
            ServiceWorkerMetrics::READ_HEADERS_ERROR);
        Done(net::URLRequestStatus(net::URLRequestStatus::FAILED, result));
        NotifyStartError(
            net::URLRequestStatus(net::URLRequestStatus::FAILED, result));
        return;
    }
    DCHECK_GE(result, 0);
    http_info_.reset(http_info_io_buffer_->http_info.release());
    if (is_range_request())
        SetupRangeResponse(http_info_io_buffer_->response_data_size);
    http_info_io_buffer_ = nullptr;
    if (is_main_script())
        version_->SetMainScriptHttpResponseInfo(*http_info_);
    TRACE_EVENT_ASYNC_END1("ServiceWorker",
        "ServiceWorkerReadFromCacheJob::ReadInfo",
        this,
        "Result", result);
    NotifyHeadersComplete();
}

void ServiceWorkerReadFromCacheJob::SetupRangeResponse(int resource_size)
{
    DCHECK(is_range_request() && http_info_.get() && reader_.get());
    if (resource_size < 0 || !range_requested_.ComputeBounds(resource_size)) {
        range_requested_ = net::HttpByteRange();
        return;
    }

    DCHECK(range_requested_.IsValid());
    int offset = static_cast<int>(range_requested_.first_byte_position());
    int length = static_cast<int>(range_requested_.last_byte_position() - range_requested_.first_byte_position() + 1);

    // Tell the reader about the range to read.
    reader_->SetReadRange(offset, length);

    // Make a copy of the full response headers and fix them up
    // for the range we'll be returning.
    range_response_info_.reset(new net::HttpResponseInfo(*http_info_));
    net::HttpResponseHeaders* headers = range_response_info_->headers.get();
    headers->UpdateWithNewRange(
        range_requested_, resource_size, true /* replace status line */);
}

void ServiceWorkerReadFromCacheJob::Done(const net::URLRequestStatus& status)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    if (!status.is_success()) {
        version_->SetStartWorkerStatusCode(SERVICE_WORKER_ERROR_DISK_CACHE);
        // TODO(falken): Retry before evicting.
        if (context_) {
            ServiceWorkerRegistration* registration = context_->GetLiveRegistration(version_->registration_id());
            registration->DeleteVersion(version_);
        }
    }
    if (is_main_script())
        version_->embedded_worker()->OnScriptReadFinished();
}

void ServiceWorkerReadFromCacheJob::OnReadComplete(int result)
{
    ServiceWorkerMetrics::ReadResponseResult check_result;

    if (result >= 0) {
        check_result = ServiceWorkerMetrics::READ_OK;
        if (result == 0)
            Done(net::URLRequestStatus());
    } else {
        check_result = ServiceWorkerMetrics::READ_DATA_ERROR;
        Done(net::URLRequestStatus(net::URLRequestStatus::FAILED, result));
    }

    ServiceWorkerMetrics::CountReadResponseResult(check_result);
    ReadRawDataComplete(result);
    TRACE_EVENT_ASYNC_END1("ServiceWorker",
        "ServiceWorkerReadFromCacheJob::ReadRawData",
        this,
        "Result", result);
}

} // namespace content
